/**
   	gvalid - the JSR-250 wrapper for performing interface-based, primitives-supported validation.
   	
    Copyright (C) 2014  Grzegorz Gurgul grzegorz.gurgul@xanthreal.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.xanthreal.gvalid;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;

public class DefaultAnnotationAdapterFactory implements AnnotationAdapterFactory {

	private ClassNameGenerator classNameGenerator;

	public DefaultAnnotationAdapterFactory(ClassNameGenerator classNameGenerator) {
		this.classNameGenerator = classNameGenerator;
	}

	public <T> AnnotationAdapter<T> createAnnotationAdapterForObject(Class<?> clazz, T object,
			Annotation[] allConstraintedAnnotations) {
		Class<?> annotationAdapterClass = null;
		try {
			annotationAdapterClass = extractClass(clazz, allConstraintedAnnotations);
		} catch (ClassNotFoundException e) {
			annotationAdapterClass = createClass(clazz, allConstraintedAnnotations);
		}

		try {
			@SuppressWarnings("unchecked")
			Constructor<AnnotationAdapter<T>> constructor = (Constructor<AnnotationAdapter<T>>) annotationAdapterClass
					.getConstructor(clazz);
			return constructor.newInstance(object);
		} catch (Exception e) {
			throw new IllegalStateException("Cannot construct the instance of the generated wrapper class", e);
		}
	}

	@SuppressWarnings("unchecked")
	private synchronized <T> Class<T> createClass(Class<T> fieldClass, Annotation[] allConstraintedAnnotations) {
		ClassPool pool = ClassPool.getDefault();
		pool.insertClassPath(new ClassClassPath(this.getClass()));
		String className = classNameGenerator.getNameForClass(fieldClass, allConstraintedAnnotations);

		try {
			CtClass wrappeeClass = pool.get(fieldClass.getCanonicalName());
			CtClass annotationAdapterClass = pool.get(AnnotationAdapter.class.getCanonicalName());
			CtClass wrapperClass = pool.makeClass(className);

			CtClass ctFieldClass = null;
			ctFieldClass = pool.get(fieldClass.getCanonicalName());
			CtField field = new CtField(ctFieldClass, "wrappee", wrapperClass);
			field.setModifiers(Modifier.PRIVATE);
			wrapperClass.addField(field);
			ClassFile ccFile = wrapperClass.getClassFile();
			ConstPool constpool = ccFile.getConstPool();
			AnnotationsAttribute attr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);

			for (int i = 0; i < allConstraintedAnnotations.length; i++) {
				Class<?> annotationToApplyClass = allConstraintedAnnotations[i].annotationType();
				javassist.bytecode.annotation.Annotation annot = null;
				annot = new javassist.bytecode.annotation.Annotation(annotationToApplyClass.getCanonicalName(),
						constpool);
				attr.addAnnotation(annot);
			}
			field.getFieldInfo().addAttribute(attr);

			wrapperClass.setInterfaces(new CtClass[] { annotationAdapterClass });

			CtMethod getAnnotatedObjectMethod = CtNewMethod.make("public " + ctFieldClass.getSimpleName()
					+ " getAnnotatedObject() { return wrappee; }", wrapperClass);
			wrapperClass.addMethod(getAnnotatedObjectMethod);

			String wrapperClassName = wrapperClass.getSimpleName();
			String wrappeeClassName = wrappeeClass.getName();

			CtConstructor wrapperConstructor = CtNewConstructor.make("public " + wrapperClassName + "("
					+ wrappeeClassName + " wrappee) { this.wrappee = wrappee; }", wrapperClass);
			wrapperClass.addConstructor(wrapperConstructor);
			Class<T> createdClass = wrapperClass.toClass();
			wrapperClass.detach();
			return createdClass;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	public Class<?> extractClass(Class<?> clazz, Annotation[] allConstraintedAnnotations) throws ClassNotFoundException {
		ClassLoader cls = ClassPool.getDefault().getClassLoader();
		String className = classNameGenerator.getNameForClass(clazz, allConstraintedAnnotations);
		return (Class<?>) cls.loadClass(className);
	}

}
